/**
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

package pittypat.web;

import javax.websocket.EndpointConfig;
import javax.websocket.HandshakeResponse;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import pittypat.Permission;
import pittypat.configuration.PittypatConfig;

/**
 * 使用 WebSocket 处理功能请求或文件上传。
 */
@ServerEndpoint(value = "/{developer}/{app}/{module}/{function}", subprotocols = { "",
		"upload.pittypat" }, configurator = WebSocketEndPoint.Configurator.class)
public final class WebSocketEndPoint {
	private static final Logger LOGGER = LogManager.getLogger(WebSocketEndPoint.class);
	// WebSocket 连接最大空闲的时间，单位：毫秒。
	private static final int MAX_IDLE_TIMEOUT = 5000;
	// 内部存储 WebSocket 对象的键值
	private static final String WEB_SOCKET_KEY = "__WEB_SOCKET";

	/**
	 * 在打开 WebSocket 连接后要首先执行的方法。 该方法会根据路径查找目标功能，对当前用户进行权限检查，并设置上下文环境变量。
	 * 
	 * @param session
	 *            WebSocket 会话。
	 * @param config
	 *            服务器端配置。
	 * @return 初始化成功还是失败。
	 */
	private boolean open(Session session, EndpointConfig config) {
		try {
			// 在 WebSocket 握手期间已经创建请求上下文，并保存在了 config 中，在打开一个新连接后，将请求上下文复制到
			// session 中，以便接下来能够从 session 中获取请求上下文对象
			RequestContext context = RequestContext.copy(config, session);

			if (context == null) {
				// 服务器没有正确设置用户信息
				WebSocket.sendError(session, "未能创建和设置请求上下文。");

				// 初始化失败，指示关闭连接
				return false;
			}

			// 获取请求路径，第一个元素是去掉 appPath 后的完整路径，其他分别为：开发者代码、App 标识符、模块标识符和功能标识符。
			// 如果是 /user/* 或 /notification/*，则返回的数组只有三个元素
			String[] pathParts = context.getPathParts();

			if (pathParts == null || pathParts.length == 0) {
				// 无效的请求路径。
				WebSocket.sendError(session, "没有提供有效的请求功能路径。");

				// 初始化失败，指示关闭连接
				return false;
			}

			// 目标功能的信息
			FunctionInfo info = context.getFuncInfo();
			if (info == null) {
				// 没有找到目标功能
				WebSocket.sendError(session, "没有找到要请求的功能。");

				// 初始化失败，指示关闭连接
				return false;
			}

			// 检查模块的状态
			if (info.moduleInfo.isMaintenance) {
				// 指示目标模块正在维护当中
				WebSocket.sendError(session,
						"目标功能 " + pathParts[0] + " 正在维护当中，暂时不可访问：\r\n"
								+ (info.moduleInfo.maintenanceMessage == null
										|| info.moduleInfo.maintenanceMessage.length() == 0 ? "维护结束后将恢复正常，请稍后再试。"
												: info.moduleInfo.maintenanceMessage));

				// 初始化失败，指示关闭连接
				return false;
			}

			// 检查用户的权限
			Permission permission = context.getPermission();
			if (!permission.isAuthorized()) {
				// 没有通过权限检查
				if (permission.getUser().getId() != null) {
					// 非匿名用户
					WebSocket.sendError(session, "当前用户没有权限访问 " + pathParts[0] + "。\r\n请询问系统管理人员您的权限，必要时可由管理员为您分配该权限。");
				} else {
					// 匿名用户
					WebSocket.sendError(session, "用户未登录或会话已经过期，没有权限访问 " + pathParts[0] + "。\r\n请重新登录。");
				}

				// 初始化失败，指示关闭连接
				return false;
			}

			// 设置 WebSocket 最大空闲的时间
			session.setMaxIdleTimeout(MAX_IDLE_TIMEOUT);
			// 缓冲区尺寸，在 jetty 中发现客户端发送数据为 8192 字节时，报告的实际大小为 8194 或
			// 8198，略大一些，因此将缓冲区尺寸设置为配置值的两倍
			// org.eclipse.jetty.websocket.api.MessageTooLargeException: Binary
			// message size [8194] exceeds maximum size [8192]
			int bufferSize = PittypatConfig.getCurrent().getSocketDataSize() * 1024 * 2;
			// 设置 WebSocket 文本消息缓冲区的尺寸
			session.setMaxTextMessageBufferSize(bufferSize);
			// 设置 WebSocket 二进制消息缓冲区的尺寸
			session.setMaxBinaryMessageBufferSize(bufferSize);

			// 初始化成功
			return true;
		} catch (Exception exc) {
			// 在请求处理过程中发生异常
			LOGGER.error("处理请求 '" + session.getRequestURI().toString() + "' 时发生错误：\r\n" + exc.getLocalizedMessage(),
					exc);
			try {
				WebSocket.sendError(session, exc);
			} catch (Exception e) {
				LOGGER.error(e.getLocalizedMessage(), e);
			}

			// 初始化失败，指示关闭连接
			return false;
		}
	}

	/**
	 * 当连接打开时执行的事件。
	 * 
	 * @param session
	 *            目标会话。
	 * @param config
	 *            服务器节点配置。
	 */
	@OnOpen
	public void onOpen(Session session, EndpointConfig config) {
		if (!this.open(session, config)) {
			// 初始化失败，关闭连接
			try {
				session.close();
			} catch (Exception e) {
				LOGGER.error(e.getLocalizedMessage(), e);
			}
		} else {
			// 初始化成功
			// WebSocket 处理程序
			WebSocket socket = null;

			// 检查是否正在上传文件
			if (UploadSocket.UPLOAD_SUB_PROTOCOL.equalsIgnoreCase(session.getNegotiatedSubprotocol())) {
				// 正在上传文件
				socket = new UploadSocket(session);
			} else {
				// 处理功能请求
				socket = new FunctionSocket(session);
			}

			// 打开操作
			int code = socket.onOpen();
			if (code != 0) {
				// 打开连接失败，关闭连接
				socket.close(code);
			} else {
				// 需要继续接收数据，将 socket 保存到 session 中
				session.getUserProperties().put(WEB_SOCKET_KEY, socket);
			}
		}
	}

	/**
	 * 接收到新的消息，该消息实际上是目标功能使用的参数列表。
	 * 
	 * @param session
	 *            目标会话。
	 * @param message
	 *            文本消息。
	 * @param last
	 *            是否最后一部分。
	 */
	@OnMessage
	public void onMessage(Session session, String message, boolean last) {
		WebSocket socket = (WebSocket) session.getUserProperties().get(WEB_SOCKET_KEY);
		if (socket != null) {
			// 接收并处理文本消息
			int code = socket.onTextMessage(message, last);

			if (code != 0) {
				// 处理消息失败，或者处理已经正常结束
				socket.close(code);
			}
		} else {
			// 没有提供有效的 WebSocket，关闭连接
			try {
				session.close();
			} catch (Exception e) {
				LOGGER.error(e.getLocalizedMessage(), e);
			}
		}
	}

	/**
	 * 接收到新的消息，该消息实际上是目标功能使用的参数列表。
	 * 
	 * @param session
	 *            目标会话。
	 * @param message
	 *            二进制消息。
	 * @param last
	 *            是否最后一部分。
	 */
	@OnMessage
	public void onMessage(Session session, byte[] message, boolean last) {
		WebSocket socket = (WebSocket) session.getUserProperties().get(WEB_SOCKET_KEY);
		if (socket != null) {
			// 接收并处理二进制消息
			int code = socket.onBinaryMessage(message, last);

			if (code != 0) {
				// 处理消息失败，或者处理已经正常结束
				socket.close(code);
			}
		} else {
			// 没有提供有效的 WebSocket，关闭连接
			try {
				session.close();
			} catch (Exception e) {
				LOGGER.error(e.getLocalizedMessage(), e);
			}
		}
	}

	/**
	 * 当连接被关闭时执行的事件。
	 * 
	 * @param session
	 *            目标会话。
	 */
	@OnClose
	public void onClose(Session session) {
		try {
			WebSocket socket = (WebSocket) session.getUserProperties().get(WEB_SOCKET_KEY);
			if (socket != null) {
				// 执行 WebSocket 的关闭事件
				socket.onClose();
				// 从 session 中移除 socket
				session.getUserProperties().remove(WEB_SOCKET_KEY);
			}
		} catch (Exception e) {
			LOGGER.error(e.getLocalizedMessage(), e);
		}
	}

	/**
	 * 当连接上发生错误时执行的事件。
	 * 
	 * @param session
	 *            目标会话。
	 * @param error
	 *            已经产生的异常。
	 */
	@OnError
	public void onError(Session session, Throwable error) {
		try {
			WebSocket socket = (WebSocket) session.getUserProperties().get(WEB_SOCKET_KEY);
			if (socket != null) {
				// 执行 WebSocket 的错误事件
				int code = socket.onError(error);

				if (code != 0) {
					// 执行错误事件发生错误，或者要求关闭连接
					socket.close(code);
				}
			}
		} catch (Exception e) {
			LOGGER.error(e.getLocalizedMessage(), e);
		}
	}

	/**
	 * 在握手期间，根据请求查找目标功能，进行权限验证，加载用户信息。
	 */
	public static final class Configurator extends ServerEndpointConfig.Configurator {
		/**
		 * 在握手期间创建请求上下文。
		 */
		@Override
		public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
			try {
				RequestContext.setupForWebSocket(request, config);
			} catch (Exception e) {
				LOGGER.error(e.getLocalizedMessage(), e);
			}
		}
	}
}
